{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<font color=\"red\">注</font>: 使用 tensorboard 可视化需要安装 tensorflow (TensorBoard依赖于tensorflow库，可以任意安装tensorflow的gpu/cpu版本)\n",
    "\n",
    "```shell\n",
    "pip install tensorflow-cpu\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:37.505092Z",
     "start_time": "2025-01-20T06:39:28.540647Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:42.791333Z",
     "iopub.status.busy": "2025-02-27T12:43:42.791177Z",
     "iopub.status.idle": "2025-02-27T12:43:44.876714Z",
     "shell.execute_reply": "2025-02-27T12:43:44.876145Z",
     "shell.execute_reply.started": "2025-02-27T12:43:42.791312Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备\n",
    "\n",
    "```shell\n",
    "$ tree -L 2 archive \n",
    "archive\n",
    "├── monkey_labels.txt\n",
    "├── training\n",
    "│   ├── n0\n",
    "│   ├── n1\n",
    "│   ├── n2\n",
    "│   ├── n3\n",
    "│   ├── n4\n",
    "│   ├── n5\n",
    "│   ├── n6\n",
    "│   ├── n7\n",
    "│   ├── n8\n",
    "│   └── n9\n",
    "└── validation\n",
    "    ├── n0\n",
    "    ├── n1\n",
    "    ├── n2\n",
    "    ├── n3\n",
    "    ├── n4\n",
    "    ├── n5\n",
    "    ├── n6\n",
    "    ├── n7\n",
    "    ├── n8\n",
    "    └── n9\n",
    "\n",
    "22 directories, 1 file\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.752355Z",
     "start_time": "2025-01-20T06:39:37.505092Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:44.878306Z",
     "iopub.status.busy": "2025-02-27T12:43:44.877867Z",
     "iopub.status.idle": "2025-02-27T12:43:45.841403Z",
     "shell.execute_reply": "2025-02-27T12:43:45.840861Z",
     "shell.execute_reply.started": "2025-02-27T12:43:44.878283Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR1 = Path(\"/content/training/\")\n",
    "DATA_DIR2 = Path(\"/content/validation/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR1 / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR2 / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.756387Z",
     "start_time": "2025-01-20T06:39:38.752355Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:45.842332Z",
     "iopub.status.busy": "2025-02-27T12:43:45.841999Z",
     "iopub.status.idle": "2025-02-27T12:43:45.846957Z",
     "shell.execute_reply": "2025-02-27T12:43:45.846406Z",
     "shell.execute_reply.started": "2025-02-27T12:43:45.842311Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.775815Z",
     "start_time": "2025-01-20T06:39:38.756387Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:45.847928Z",
     "iopub.status.busy": "2025-02-27T12:43:45.847520Z",
     "iopub.status.idle": "2025-02-27T12:43:45.857036Z",
     "shell.execute_reply": "2025-02-27T12:43:45.856527Z",
     "shell.execute_reply.started": "2025-02-27T12:43:45.847907Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/content/training/training/n0/n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.778866Z",
     "start_time": "2025-01-20T06:39:38.775815Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:45.857795Z",
     "iopub.status.busy": "2025-02-27T12:43:45.857600Z",
     "iopub.status.idle": "2025-02-27T12:43:45.861159Z",
     "shell.execute_reply": "2025-02-27T12:43:45.860682Z",
     "shell.execute_reply.started": "2025-02-27T12:43:45.857776Z"
    }
   },
   "outputs": [],
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:39:38.781799Z",
     "start_time": "2025-01-20T06:39:38.778866Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:45.863197Z",
     "iopub.status.busy": "2025-02-27T12:43:45.862888Z",
     "iopub.status.idle": "2025-02-27T12:43:45.866224Z",
     "shell.execute_reply": "2025-02-27T12:43:45.865757Z",
     "shell.execute_reply.started": "2025-02-27T12:43:45.863178Z"
    }
   },
   "outputs": [],
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:19:28.364598500Z",
     "start_time": "2024-07-23T02:19:27.176829700Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:45.866796Z",
     "iopub.status.busy": "2025-02-27T12:43:45.866616Z",
     "iopub.status.idle": "2025-02-27T12:43:46.060976Z",
     "shell.execute_reply": "2025-02-27T12:43:46.060438Z",
     "shell.execute_reply.started": "2025-02-27T12:43:45.866778Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:28:55.526142Z",
     "start_time": "2025-01-20T07:28:55.358597Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:46.061946Z",
     "iopub.status.busy": "2025-02-27T12:43:46.061660Z",
     "iopub.status.idle": "2025-02-27T12:43:46.066915Z",
     "shell.execute_reply": "2025-02-27T12:43:46.066415Z",
     "shell.execute_reply.started": "2025-02-27T12:43:46.061925Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重\n",
    "        # for param in self.model.layer4.parameters():\n",
    "        #     param.requires_grad = True  # 解冻 layer4\n",
    "        # for name, param in self.model.named_parameters():\n",
    "        #     if name == \"layer4.2.conv3.weight\":\n",
    "        #         param.requires_grad = True  # 解冻该层\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "# for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:28:58.912249Z",
     "start_time": "2025-01-20T07:28:58.749737Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:43:46.067905Z",
     "iopub.status.busy": "2025-02-27T12:43:46.067475Z",
     "iopub.status.idle": "2025-02-27T12:45:12.476163Z",
     "shell.execute_reply": "2025-02-27T12:45:12.475662Z",
     "shell.execute_reply.started": "2025-02-27T12:43:46.067884Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading: \"https://download.pytorch.org/models/resnet50-11ad3fa6.pth\" to /root/.cache/torch/hub/checkpoints/resnet50-11ad3fa6.pth\n",
      "100%|██████████| 97.8M/97.8M [01:23<00:00, 1.22MB/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "20490"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)\n",
    "def count_parameters(model): #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "count_parameters(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T07:08:10.086234Z",
     "start_time": "2025-01-20T07:08:10.083273Z"
    }
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:48:29.601561Z",
     "start_time": "2025-01-20T06:48:29.597356Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.476960Z",
     "iopub.status.busy": "2025-02-27T12:45:12.476765Z",
     "iopub.status.idle": "2025-02-27T12:45:12.482147Z",
     "shell.execute_reply": "2025-02-27T12:45:12.481631Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.476941Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:48:57.859769Z",
     "start_time": "2025-01-20T06:48:57.855987Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.482875Z",
     "iopub.status.busy": "2025-02-27T12:45:12.482652Z",
     "iopub.status.idle": "2025-02-27T12:45:12.486112Z",
     "shell.execute_reply": "2025-02-27T12:45:12.485682Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.482856Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T06:52:19.752248Z",
     "start_time": "2025-01-20T06:52:19.747379Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.487044Z",
     "iopub.status.busy": "2025-02-27T12:45:12.486610Z",
     "iopub.status.idle": "2025-02-27T12:45:12.492071Z",
     "shell.execute_reply": "2025-02-27T12:45:12.491533Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.487026Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T07:06:55.092794100Z",
     "start_time": "2024-07-23T07:06:55.088043800Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.492820Z",
     "iopub.status.busy": "2025-02-27T12:45:12.492618Z",
     "iopub.status.idle": "2025-02-27T12:45:12.496050Z",
     "shell.execute_reply": "2025-02-27T12:45:12.495588Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.492801Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2359296"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*3*3*512"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:45:55.270615100Z",
     "start_time": "2024-07-23T03:45:55.261439200Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.496901Z",
     "iopub.status.busy": "2025-02-27T12:45:12.496562Z",
     "iopub.status.idle": "2025-02-27T12:45:12.499969Z",
     "shell.execute_reply": "2025-02-27T12:45:12.499477Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.496882Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1048576"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*1*1*2048"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:56:07.349270600Z",
     "start_time": "2024-07-23T02:56:07.337722300Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.500608Z",
     "iopub.status.busy": "2025-02-27T12:45:12.500451Z",
     "iopub.status.idle": "2025-02-27T12:45:12.503732Z",
     "shell.execute_reply": "2025-02-27T12:45:12.503323Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.500590Z"
    },
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32.0"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512/16"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:12.386898800Z",
     "start_time": "2024-07-23T02:59:11.605525600Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.504593Z",
     "iopub.status.busy": "2025-02-27T12:45:12.504241Z",
     "iopub.status.idle": "2025-02-27T12:45:12.530037Z",
     "shell.execute_reply": "2025-02-27T12:45:12.529592Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.504575Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.530989Z",
     "iopub.status.busy": "2025-02-27T12:45:12.530565Z",
     "iopub.status.idle": "2025-02-27T12:45:12.588459Z",
     "shell.execute_reply": "2025-02-27T12:45:12.588014Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.530969Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:18.296416Z",
     "start_time": "2024-07-23T02:59:18.287854800Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.590961Z",
     "iopub.status.busy": "2025-02-27T12:45:12.590561Z",
     "iopub.status.idle": "2025-02-27T12:45:12.595617Z",
     "shell.execute_reply": "2025-02-27T12:45:12.595171Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.590941Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T02:59:21.080731100Z",
     "start_time": "2024-07-23T02:59:21.073276Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.596311Z",
     "iopub.status.busy": "2025-02-27T12:45:12.596135Z",
     "iopub.status.idle": "2025-02-27T12:45:12.600377Z",
     "shell.execute_reply": "2025-02-27T12:45:12.599940Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.596293Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:00:28.829465300Z",
     "start_time": "2024-07-23T02:59:52.601793600Z"
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:45:12.601184Z",
     "iopub.status.busy": "2025-02-27T12:45:12.601017Z",
     "iopub.status.idle": "2025-02-27T12:48:03.570023Z",
     "shell.execute_reply": "2025-02-27T12:48:03.569376Z",
     "shell.execute_reply.started": "2025-02-27T12:45:12.601166Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 35%|███▌      | 483/1380 [02:50<05:16,  2.83it/s, epoch=6]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 7 / global_step 483\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:48:03.571399Z",
     "iopub.status.busy": "2025-02-27T12:48:03.570844Z",
     "iopub.status.idle": "2025-02-27T12:48:08.093946Z",
     "shell.execute_reply": "2025-02-27T12:48:08.093322Z",
     "shell.execute_reply.started": "2025-02-27T12:48:03.571366Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Looking in indexes: https://mirrors.cloud.aliyuncs.com/pypi/simple\n",
      "Requirement already satisfied: torchviz in /usr/local/lib/python3.10/site-packages (0.0.3)\n",
      "Requirement already satisfied: torch in /usr/local/lib/python3.10/site-packages (from torchviz) (2.5.1)\n",
      "Requirement already satisfied: graphviz in /usr/local/lib/python3.10/site-packages (from torchviz) (0.20.3)\n",
      "Requirement already satisfied: filelock in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (3.16.1)\n",
      "Requirement already satisfied: typing-extensions>=4.8.0 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (4.12.2)\n",
      "Requirement already satisfied: networkx in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (3.4.2)\n",
      "Requirement already satisfied: jinja2 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (3.1.5)\n",
      "Requirement already satisfied: fsspec in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (2024.6.1)\n",
      "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.4.127 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.4.127)\n",
      "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.4.127 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.4.127)\n",
      "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.4.127 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.4.127)\n",
      "Requirement already satisfied: nvidia-cudnn-cu12==9.1.0.70 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (9.1.0.70)\n",
      "Requirement already satisfied: nvidia-cublas-cu12==12.4.5.8 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.4.5.8)\n",
      "Requirement already satisfied: nvidia-cufft-cu12==11.2.1.3 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (11.2.1.3)\n",
      "Requirement already satisfied: nvidia-curand-cu12==10.3.5.147 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (10.3.5.147)\n",
      "Requirement already satisfied: nvidia-cusolver-cu12==11.6.1.9 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (11.6.1.9)\n",
      "Requirement already satisfied: nvidia-cusparse-cu12==12.3.1.170 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.3.1.170)\n",
      "Requirement already satisfied: nvidia-nccl-cu12==2.21.5 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (2.21.5)\n",
      "Requirement already satisfied: nvidia-nvtx-cu12==12.4.127 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.4.127)\n",
      "Requirement already satisfied: nvidia-nvjitlink-cu12==12.4.127 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (12.4.127)\n",
      "Requirement already satisfied: triton==3.1.0 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (3.1.0)\n",
      "Requirement already satisfied: sympy==1.13.1 in /usr/local/lib/python3.10/site-packages (from torch->torchviz) (1.13.1)\n",
      "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.10/site-packages (from sympy==1.13.1->torch->torchviz) (1.3.0)\n",
      "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/site-packages (from jinja2->torch->torchviz) (2.1.5)\n",
      "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n",
      "\u001b[0m\n",
      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.0.1\u001b[0m\n",
      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n"
     ]
    }
   ],
   "source": [
    "!pip install torchviz"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-23T03:02:10.300044700Z",
     "start_time": "2024-07-23T03:02:09.443403Z"
    },
    "collapsed": false,
    "execution": {
     "iopub.execute_input": "2025-02-27T12:48:08.094982Z",
     "iopub.status.busy": "2025-02-27T12:48:08.094709Z",
     "iopub.status.idle": "2025-02-27T12:48:08.776685Z",
     "shell.execute_reply": "2025-02-27T12:48:08.775986Z",
     "shell.execute_reply.started": "2025-02-27T12:48:08.094961Z"
    },
    "jupyter": {
     "outputs_hidden": false
    },
    "tags": []
   },
   "outputs": [
    {
     "ename": "RuntimeError",
     "evalue": "Input type (torch.FloatTensor) and weight type (torch.cuda.FloatTensor) should be the same or input should be a MKLDNN tensor and weight is a dense tensor",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mRuntimeError\u001b[0m                              Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[22], line 11\u001b[0m\n\u001b[1;32m      8\u001b[0m dummy_input \u001b[38;5;241m=\u001b[39m torch\u001b[38;5;241m.\u001b[39mrandn(\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m224\u001b[39m, \u001b[38;5;241m224\u001b[39m)  \u001b[38;5;66;03m# Replace with your input shape\u001b[39;00m\n\u001b[1;32m     10\u001b[0m \u001b[38;5;66;03m# Forward pass to generate the computation graph\u001b[39;00m\n\u001b[0;32m---> 11\u001b[0m output \u001b[38;5;241m=\u001b[39m \u001b[43mmodel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdummy_input\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m     13\u001b[0m \u001b[38;5;66;03m# Visualize the model architecture\u001b[39;00m\n\u001b[1;32m     14\u001b[0m dot \u001b[38;5;241m=\u001b[39m make_dot(output, params\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mdict\u001b[39m(model\u001b[38;5;241m.\u001b[39mnamed_parameters()))\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1736\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1734\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)  \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[1;32m   1735\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 1736\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_impl\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1747\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1742\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[1;32m   1743\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[1;32m   1744\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[1;32m   1745\u001b[0m         \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[1;32m   1746\u001b[0m         \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[0;32m-> 1747\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mforward_call\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   1749\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m   1750\u001b[0m called_always_called_hooks \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mset\u001b[39m()\n",
      "Cell \u001b[0;32mIn[8], line 27\u001b[0m, in \u001b[0;36mResNet50.forward\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m     26\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mforward\u001b[39m(\u001b[38;5;28mself\u001b[39m, x):\n\u001b[0;32m---> 27\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmodel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1736\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1734\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)  \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[1;32m   1735\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 1736\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_impl\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1747\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1742\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[1;32m   1743\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[1;32m   1744\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[1;32m   1745\u001b[0m         \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[1;32m   1746\u001b[0m         \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[0;32m-> 1747\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mforward_call\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   1749\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m   1750\u001b[0m called_always_called_hooks \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mset\u001b[39m()\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torchvision/models/resnet.py:285\u001b[0m, in \u001b[0;36mResNet.forward\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m    284\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mforward\u001b[39m(\u001b[38;5;28mself\u001b[39m, x: Tensor) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tensor:\n\u001b[0;32m--> 285\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_forward_impl\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torchvision/models/resnet.py:268\u001b[0m, in \u001b[0;36mResNet._forward_impl\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m    266\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21m_forward_impl\u001b[39m(\u001b[38;5;28mself\u001b[39m, x: Tensor) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tensor:\n\u001b[1;32m    267\u001b[0m     \u001b[38;5;66;03m# See note [TorchScript super()]\u001b[39;00m\n\u001b[0;32m--> 268\u001b[0m     x \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconv1\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m    269\u001b[0m     x \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbn1(x)\n\u001b[1;32m    270\u001b[0m     x \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrelu(x)\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1736\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1734\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)  \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[1;32m   1735\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 1736\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_impl\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:1747\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1742\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[1;32m   1743\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[1;32m   1744\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[1;32m   1745\u001b[0m         \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[1;32m   1746\u001b[0m         \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[0;32m-> 1747\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mforward_call\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   1749\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m   1750\u001b[0m called_always_called_hooks \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mset\u001b[39m()\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/conv.py:554\u001b[0m, in \u001b[0;36mConv2d.forward\u001b[0;34m(self, input)\u001b[0m\n\u001b[1;32m    553\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mforward\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;28minput\u001b[39m: Tensor) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tensor:\n\u001b[0;32m--> 554\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_conv_forward\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mweight\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbias\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/lib/python3.10/site-packages/torch/nn/modules/conv.py:549\u001b[0m, in \u001b[0;36mConv2d._conv_forward\u001b[0;34m(self, input, weight, bias)\u001b[0m\n\u001b[1;32m    537\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpadding_mode \u001b[38;5;241m!=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mzeros\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m    538\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m F\u001b[38;5;241m.\u001b[39mconv2d(\n\u001b[1;32m    539\u001b[0m         F\u001b[38;5;241m.\u001b[39mpad(\n\u001b[1;32m    540\u001b[0m             \u001b[38;5;28minput\u001b[39m, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_reversed_padding_repeated_twice, mode\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpadding_mode\n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m    547\u001b[0m         \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgroups,\n\u001b[1;32m    548\u001b[0m     )\n\u001b[0;32m--> 549\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mF\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconv2d\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m    550\u001b[0m \u001b[43m    \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mweight\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbias\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstride\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpadding\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdilation\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroups\u001b[49m\n\u001b[1;32m    551\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n",
      "\u001b[0;31mRuntimeError\u001b[0m: Input type (torch.FloatTensor) and weight type (torch.cuda.FloatTensor) should be the same or input should be a MKLDNN tensor and weight is a dense tensor"
     ]
    }
   ],
   "source": [
    "# 画图\n",
    "\n",
    "import torch\n",
    "from torchviz import make_dot\n",
    "\n",
    "# Assuming your model is already defined and named 'model'\n",
    "# Construct a dummy input\n",
    "dummy_input = torch.randn(1, 3, 224, 224)  # Replace with your input shape\n",
    "\n",
    "# Forward pass to generate the computation graph\n",
    "output = model(dummy_input)\n",
    "\n",
    "# Visualize the model architecture\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "dot.render(\"model_architecture\", format=\"png\")  # Save the visualization as an image\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-02-27T12:49:33.364131Z",
     "iopub.status.busy": "2025-02-27T12:49:33.363769Z",
     "iopub.status.idle": "2025-02-27T12:49:33.566272Z",
     "shell.execute_reply": "2025-02-27T12:49:33.565659Z",
     "shell.execute_reply.started": "2025-02-27T12:49:33.364108Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "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Y8eOFYGBgUKlUolWrVqJsWPHinPnzln2O0MbNEfsm9g3mTnD7yD7pdo5Y78kRPPqm2RCCHFvY0lERERERET2z2GvGSIiIiIiIroTJkNEREREROSUmAwREREREZFTYjJEREREREROickQERERERE5JSZDRERERETklJgMERERERGRU2IyRERERERETonJEFEDmTBhAkaOHGnrMIiIiACwXyKqCyZDRERERETklJgMEd2jH3/8EV27doVGo4G3tzcGDhyIN954A19//TXWrVsHmUwGmUyGhIQEAEBGRgaeeeYZ6PV6eHl5YcSIEUhLS7O8nvmbu9mzZ8PX1xfu7u6YPHkyysvLbXOCRERkV9gvEdWfi60DILInV65cwbhx47BgwQKMGjUKRUVF2LlzJ/7nf/4H6enpKCwsxMqVKwEAXl5eMBqNGDx4MKKjo7Fz5064uLjgo48+wpAhQ3D06FGoVCoAwJYtW+Dq6oqEhASkpaXh+eefh7e3N+bOnWvL0yUiomaO/RLR/WEyRHQPrly5goqKCowePRpt2rQBAHTt2hUAoNFoUFZWhoCAAMvx33zzDUwmE7788kvIZDIAwMqVK6HX65GQkIBBgwYBAFQqFVasWAGtVovOnTtjzpw5eOONN/Dhhx9CLucALhER1Yz9EtH94U8z0T2IjIxETEwMunbtiqeffhr/+Mc/kJ+fX+vxycnJOHfuHNzc3KDT6aDT6eDl5YXS0lKkpqZava5Wq7Xcj46OhsFgQEZGRqOeDxER2Tf2S0T3hyNDRPdAoVAgPj4ee/bswaZNm7Bs2TK899572L9/f43HGwwGREVFYdWqVbft8/X1bexwiYjIwbFfIro/TIaI7pFMJkO/fv3Qr18/fPDBB2jTpg3Wrl0LlUqFyspKq2N79OiB//znP/Dz84O7u3utr5mcnIySkhJoNBoAwL59+6DT6dC6detGPRciIrJ/7JeI6o/T5Ijuwf79+/Hxxx/j0KFDSE9Px5o1a5CTk4OOHTsiJCQER48eRUpKCnJzc2E0GhEXFwcfHx+MGDECO3fuxIULF5CQkIA//elPuHTpkuV1y8vLMXHiRJw8eRIbNmzAzJkzMW3aNM7LJiKiO2K/RHR/ODJEdA/c3d2xY8cOLF68GIWFhWjTpg0WLlyIoUOHomfPnkhISEDPnj1hMBiwbds2DBgwADt27MBbb72F0aNHo6ioCK1atUJMTIzVN3IxMTEIDw/HI488grKyMowbNw6zZs2y3YkSEZFdYL9EdH9kQghh6yCInNmECRNQUFCAn3/+2dahEBERsV8ip8KxTiIiIiIickpMhoiIiIiIyClxmhwRERERETkljgwREREREZFTYjJEREREREROickQERERERE5JSZDRERERETklJgMERERERGRU2IyRERERERETonJEBEREREROSUmQ0RERERE5JSYDBERERERkVP6/9Qb3uw8m9fdAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-02-27T12:49:35.565103Z",
     "iopub.status.busy": "2025-02-27T12:49:35.564756Z",
     "iopub.status.idle": "2025-02-27T12:49:40.635935Z",
     "shell.execute_reply": "2025-02-27T12:49:40.635224Z",
     "shell.execute_reply.started": "2025-02-27T12:49:35.565079Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.4234\n",
      "accuracy: 0.9890\n"
     ]
    }
   ],
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\",weights_only=True, map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
